// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// Package codesign为
// Mach-O文件的特殊代码签名提供基本功能。
// 
// 这不是用于代码签名的通用工具。它是
// 专门为Go工具链设计的。它使用与达尔文链接器相同的
// 即席签名算法。
package codesign

import (
	"crypto/sha256"
	"debug/macho"
	"encoding/binary"
	"io"
)

// 代码签名布局。
// 
// 代码签名是一个字节块，其中包含一个或多个Blob。对于临时
// 签名，一个CodeDirectory Blob就足够了。
// 
// SuperBlob以其头（SuperBlob结构的二进制表示
// ）开始，然后是一个Blob列表（在本例中，
// one）（偏移量和大小）。CodeDirectory Blob以其头（CodeDirectory结构的二进制表示形式）开始
// 
// 后跟标识符（作为C字符串）和散列，位于
// 对应的偏移量。
// 
// 签名数据必须包含在_LINKEDIT段中。
// 在Mach-O文件头中，LC_CODE_签名加载命令
// 指向数据。

const (
	pageSizeBits = 12
	pageSize     = 1 << pageSizeBits
)

const LC_CODE_SIGNATURE = 0x1d

// 常量和结构布局来自
// https:

const (
	CSMAGIC_REQUIREMENT        = 0xfade0c00 // 单需求blob 
	CSMAGIC_REQUIREMENTS       = 0xfade0c01 // 需求向量（内部需求）
	CSMAGIC_CODEDIRECTORY      = 0xfade0c02 // 代码目录blob 
	CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0 // 签名数据的嵌入形式
	CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc1 // 嵌入签名的多拱集合

	CSSLOT_CODEDIRECTORY = 0 // 代码目录的槽索引
)

const (
	CS_HASHTYPE_SHA1             = 1
	CS_HASHTYPE_SHA256           = 2
	CS_HASHTYPE_SHA256_TRUNCATED = 3
	CS_HASHTYPE_SHA384           = 4
)

const (
	CS_EXECSEG_MAIN_BINARY     = 0x1   // 可执行段表示主要二进制
	CS_EXECSEG_ALLOW_UNSIGNED  = 0x10  // 允许未签名页（用于调试）
	CS_EXECSEG_DEBUGGER        = 0x20  // 主二进制是调试器
	CS_EXECSEG_JIT             = 0x40  // 启用JIT的
	CS_EXECSEG_SKIP_LV         = 0x80  // 跳过库验证
	CS_EXECSEG_CAN_LOAD_CDHASH = 0x100 // 可以执行
	CS_EXECSEG_CAN_EXEC_CDHASH = 0x200 // 可以执行祝福的cdhash 
)

type Blob struct {
	typ    uint32 // 条目类型
	offset uint32 // 条目偏移量
	// 数据跟随
}

func (b *Blob) put(out []byte) []byte {
	out = put32be(out, b.typ)
	out = put32be(out, b.offset)
	return out
}

const blobSize = 2 * 4

type SuperBlob struct {
	magic  uint32 // 魔术number 
	length uint32 // SuperBlob的总长度
	count  uint32 // 遵循
	// Blob[]Blob 
}

func (s *SuperBlob) put(out []byte) []byte {
	out = put32be(out, s.magic)
	out = put32be(out, s.length)
	out = put32be(out, s.count)
	return out
}

const superBlobSize = 3 * 4

type CodeDirectory struct {
	magic         uint32 // 幻数（CSMAGIC_code directory）
	length        uint32 // code directory Blob的总长度
	version       uint32 // 兼容版本
	flags         uint32 // 设置和模式标志
	hashOffset    uint32 // 位于索引零
	identOffset   uint32 // 标识符字符串偏移量
	nSpecialSlots uint32 // 特殊哈希槽数
	nCodeSlots    uint32 // 普通（代码）哈希槽数
	codeLimit     uint32 // 限制主图像签名范围
	hashSize      uint8  // 每个哈希的大小（字节）
	hashType      uint8  // 哈希类型（cdHashType*常数）
	_pad1         uint8  // 未使用（必须为零）
	pageSize      uint8  // log2（以字节为单位的页面大小）；0=>无限
	_pad2         uint32 // 未使用（必须为零）
	scatterOffset uint32
	teamOffset    uint32
	_pad3         uint32
	codeLimit64   uint64
	execSegBase   uint64
	execSegLimit  uint64
	execSegFlags  uint64
	// 数据后面跟着
}

func (c *CodeDirectory) put(out []byte) []byte {
	out = put32be(out, c.magic)
	out = put32be(out, c.length)
	out = put32be(out, c.version)
	out = put32be(out, c.flags)
	out = put32be(out, c.hashOffset)
	out = put32be(out, c.identOffset)
	out = put32be(out, c.nSpecialSlots)
	out = put32be(out, c.nCodeSlots)
	out = put32be(out, c.codeLimit)
	out = put8(out, c.hashSize)
	out = put8(out, c.hashType)
	out = put8(out, c._pad1)
	out = put8(out, c.pageSize)
	out = put32be(out, c._pad2)
	out = put32be(out, c.scatterOffset)
	out = put32be(out, c.teamOffset)
	out = put32be(out, c._pad3)
	out = put64be(out, c.codeLimit64)
	out = put64be(out, c.execSegBase)
	out = put64be(out, c.execSegLimit)
	out = put64be(out, c.execSegFlags)
	return out
}

const codeDirectorySize = 13*4 + 4 + 4*8

// CodeSigCmd是Mach-O LC_CODE_签名加载命令。
type CodeSigCmd struct {
	Cmd      uint32 // LC_CODE_SIGNATURE 
	Cmdsize  uint32 // sizeof this command（16）
	Dataoff  uint32 // u LINKEDIT段中数据的文件偏移量
	Datasize uint32 // u LINKEDIT段中数据的文件大小
}

func FindCodeSigCmd(f *macho.File) (CodeSigCmd, bool) {
	get32 := f.ByteOrder.Uint32
	for _, l := range f.Loads {
		data := l.Raw()
		cmd := get32(data)
		if cmd == LC_CODE_SIGNATURE {
			return CodeSigCmd{
				cmd,
				get32(data[4:]),
				get32(data[8:]),
				get32(data[12:]),
			}, true
		}
	}
	return CodeSigCmd{}, false
}

func put32be(b []byte, x uint32) []byte { binary.BigEndian.PutUint32(b, x); return b[4:] }
func put64be(b []byte, x uint64) []byte { binary.BigEndian.PutUint64(b, x); return b[8:] }
func put8(b []byte, x uint8) []byte     { b[0] = x; return b[1:] }
func puts(b, s []byte) []byte           { n := copy(b, s); return b[n:] }

// size计算代码签名的大小。
// id是用于签名的标识符（CodeDirectory blob中的一个字段，
// 在临时签名中没有意义）。
func Size(codeSize int64, id string) int64 {
	nhashes := (codeSize + pageSize - 1) / pageSize
	idOff := int64(codeDirectorySize)
	hashOff := idOff + int64(len(id)+1)
	cdirSz := hashOff + nhashes*sha256.Size
	return int64(superBlobSize+blobSize) + cdirSz
}

// Sign生成一个特殊的代码签名，并将其写出来。
// out必须至少具有长度大小（codeSize，id）。
// 数据是不带签名的文件内容，大小为codeSize。
// textOff和textSize是文本段的文件偏移量和大小。
// 如果这是主可执行文件，则isMain为真。
// id是用于签名的标识符（CodeDirectory blob中的一个字段，
// 在临时签名中没有意义）。
func Sign(out []byte, data io.Reader, id string, codeSize, textOff, textSize int64, isMain bool) {
	nhashes := (codeSize + pageSize - 1) / pageSize
	idOff := int64(codeDirectorySize)
	hashOff := idOff + int64(len(id)+1)
	sz := Size(codeSize, id)

	// emit blob headers 
	sb := SuperBlob{
		magic:  CSMAGIC_EMBEDDED_SIGNATURE,
		length: uint32(sz),
		count:  1,
	}
	blob := Blob{
		typ:    CSSLOT_CODEDIRECTORY,
		offset: superBlobSize + blobSize,
	}
	cdir := CodeDirectory{
		magic:        CSMAGIC_CODEDIRECTORY,
		length:       uint32(sz) - (superBlobSize + blobSize),
		version:      0x20400,
		flags:        0x20002, // adhoc | linkerSigned 
		hashOffset:   uint32(hashOff),
		identOffset:  uint32(idOff),
		nCodeSlots:   uint32(nhashes),
		codeLimit:    uint32(codeSize),
		hashSize:     sha256.Size,
		hashType:     CS_HASHTYPE_SHA256,
		pageSize:     uint8(pageSizeBits),
		execSegBase:  uint64(textOff),
		execSegLimit: uint64(textSize),
	}
	if isMain {
		cdir.execSegFlags = CS_EXECSEG_MAIN_BINARY
	}

	outp := out
	outp = sb.put(outp)
	outp = blob.put(outp)
	outp = cdir.put(outp)

	// emit the identifier 
	outp = puts(outp, []byte(id+"\000"))

	// emit hash 
	var buf [pageSize]byte
	h := sha256.New()
	p := 0
	for p < int(codeSize) {
		n, err := io.ReadFull(data, buf[:])
		if err == io.EOF {
			break
		}
		if err != nil && err != io.ErrUnexpectedEOF {
			panic(err)
		}
		if p+n > int(codeSize) {
			n = int(codeSize) - p
		}
		p += n
		h.Reset()
		h.Write(buf[:n])
		b := h.Sum(nil)
		outp = puts(outp, b[:])
	}
}
